/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import 'vs/css!./ghostText';
import * as dom from 'vs/base/browser/dom';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import * as strings from 'vs/base/common/strings';
import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions';
import { createStringBuilder } from 'vs/editor/common/core/stringBuilder';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { LineTokens } from 'vs/editor/common/core/lineTokens';
import { Position } from 'vs/editor/common/core/position';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { ghostTextBorder, ghostTextForeground } from 'vs/editor/common/view/editorColorRegistry';
import { RGBA, Color } from 'vs/base/common/color';
import { CursorColumns } from 'vs/editor/common/controller/cursorCommon';
import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon';
import { GhostTextWidgetModel, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/ghostText';

const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value });

export class GhostTextWidget extends Disposable {
	private disposed = false;
	private readonly partsWidget = this._register(new PartsWidget(this.editor, this.codeEditorService, this.themeService));
	private readonly additionalLinesWidget = this._register(new AdditionalLinesWidget(this.editor));
	private viewMoreContentWidget: ViewMoreLinesContentWidget | undefined = undefined;

	constructor(
		private readonly editor: ICodeEditor,
		private readonly model: GhostTextWidgetModel,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
		@IThemeService private readonly themeService: IThemeService,
	) {
		super();

		this._register(this.editor.onDidChangeConfiguration((e) => {
			if (
				e.hasChanged(EditorOption.disableMonospaceOptimizations)
				|| e.hasChanged(EditorOption.stopRenderingLineAfter)
				|| e.hasChanged(EditorOption.renderWhitespace)
				|| e.hasChanged(EditorOption.renderControlCharacters)
				|| e.hasChanged(EditorOption.fontLigatures)
				|| e.hasChanged(EditorOption.fontInfo)
				|| e.hasChanged(EditorOption.lineHeight)
			) {
				this.update();
			}
		}));

		this._register(toDisposable(() => {
			this.disposed = true;
			this.update();

			this.viewMoreContentWidget?.dispose();
			this.viewMoreContentWidget = undefined;
		}));

		this._register(model.onDidChange(() => {
			this.update();
		}));
		this.update();
	}

	public shouldShowHoverAtViewZone(viewZoneId: string): boolean {
		return (this.additionalLinesWidget.viewZoneId === viewZoneId);
	}

	private update(): void {
		if (!this.editor.hasModel() || !this.model.ghostText || this.disposed) {
			this.partsWidget.clear();
			this.additionalLinesWidget.clear();
			return;
		}

		const ghostText = this.model.ghostText;
		this.partsWidget.setParts(ghostText.lineNumber, ghostText.parts);
		this.additionalLinesWidget.updateLines(ghostText.lineNumber, ghostText.additionalLines, ghostText.additionalReservedLineCount);

		if (ghostText.additionalLines.length < 0) {
			// not supported at the moment
			this.viewMoreContentWidget = this.renderViewMoreLines(
				new Position(ghostText.lineNumber, this.editor.getModel()!.getLineMaxColumn(ghostText.lineNumber)),
				'', ghostText.additionalLines.length
			);
		} else {
			this.viewMoreContentWidget?.dispose();
			this.viewMoreContentWidget = undefined;
		}
	}

	private renderViewMoreLines(position: Position, firstLineText: string, remainingLinesLength: number): ViewMoreLinesContentWidget {
		const fontInfo = this.editor.getOption(EditorOption.fontInfo);
		const domNode = document.createElement('div');
		domNode.className = 'suggest-preview-additional-widget';
		Configuration.applyFontInfoSlow(domNode, fontInfo);

		const spacer = document.createElement('span');
		spacer.className = 'content-spacer';
		spacer.append(firstLineText);
		domNode.append(spacer);

		const newline = document.createElement('span');
		newline.className = 'content-newline suggest-preview-text';
		newline.append('⏎  ');
		domNode.append(newline);

		const disposableStore = new DisposableStore();

		const button = document.createElement('div');
		button.className = 'button suggest-preview-text';
		button.append(`+${remainingLinesLength} lines…`);

		disposableStore.add(dom.addStandardDisposableListener(button, 'mousedown', (e) => {
			this.model?.setExpanded(true);
			e.preventDefault();
			this.editor.focus();
		}));

		domNode.append(button);
		return new ViewMoreLinesContentWidget(this.editor, position, domNode, disposableStore);
	}
}

class PartsWidget implements IDisposable {
	private decorationIds: string[] = [];
	private disposableStore: DisposableStore = new DisposableStore();

	constructor(
		private readonly editor: ICodeEditor,
		@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
		@IThemeService private readonly themeService: IThemeService
	) {
	}

	public dispose(): void {
		this.clear();
		this.disposableStore.dispose();
	}

	public clear(): void {
		this.editor.deltaDecorations(this.decorationIds, []);
		this.disposableStore.clear();
	}

	public setParts(lineNumber: number, parts: GhostTextPart[]): void {
		this.disposableStore.clear();

		const colorTheme = this.themeService.getColorTheme();
		const foreground = colorTheme.getColor(ghostTextForeground);
		let opacity: string | undefined = undefined;
		let color: string | undefined = undefined;
		if (foreground) {
			opacity = String(foreground.rgba.a);
			color = Color.Format.CSS.format(opaque(foreground))!;
		}

		const borderColor = colorTheme.getColor(ghostTextBorder);
		let border: string | undefined = undefined;
		if (borderColor) {
			border = `2px dashed ${borderColor}`;
		}

		const textModel = this.editor.getModel();
		if (!textModel) {
			return;
		}

		const { tabSize } = textModel.getOptions();

		const line = textModel.getLineContent(lineNumber) || '';
		let lastIndex = 0;
		let currentLinePrefix = '';

		this.decorationIds = this.editor.deltaDecorations(this.decorationIds, parts.map(p => {

			currentLinePrefix += line.substring(lastIndex, p.column - 1);
			lastIndex = p.column - 1;

			// To avoid visual confusion, we don't want to render visible whitespace
			const contentText = this.renderSingleLineText(p.text, currentLinePrefix, tabSize, false);

			const decorationType = registerDecorationType(this.codeEditorService, 'ghost-text', '0-ghost-text-', {
				after: {
					// TODO: escape?
					contentText,
					opacity,
					color,
					border,
				},
			});
			this.disposableStore.add(decorationType);
			return ({
				range: Range.fromPositions(new Position(lineNumber, p.column)),
				options: {
					...decorationType.resolve()
				}
			});
		}));
	}

	private renderSingleLineText(text: string, lineStart: string, tabSize: number, renderWhitespace: boolean): string {
		const newLine = lineStart + text;
		const visibleColumnsByColumns = CursorColumns.visibleColumnsByColumns(newLine, tabSize);


		let contentText = '';
		let curCol = lineStart.length + 1;
		for (const c of text) {
			if (c === '\t') {
				const width = visibleColumnsByColumns[curCol + 1] - visibleColumnsByColumns[curCol];
				if (renderWhitespace) {
					contentText += '→';
					for (let i = 1; i < width; i++) {
						contentText += '\xa0';
					}
				} else {
					for (let i = 0; i < width; i++) {
						contentText += '\xa0';
					}
				}
			} else if (c === ' ') {
				if (renderWhitespace) {
					contentText += '·';
				} else {
					contentText += '\xa0';
				}
			} else {
				contentText += c;
			}
			curCol += 1;
		}

		return contentText;
	}
}

function opaque(color: Color): Color {
	const { r, b, g } = color.rgba;
	return new Color(new RGBA(r, g, b, 255));
}

class AdditionalLinesWidget implements IDisposable {
	private _viewZoneId: string | undefined = undefined;
	public get viewZoneId(): string | undefined { return this._viewZoneId; }

	constructor(private readonly editor: ICodeEditor) { }

	public dispose(): void {
		this.clear();
	}

	public clear(): void {
		this.editor.changeViewZones((changeAccessor) => {
			if (this._viewZoneId) {
				changeAccessor.removeZone(this._viewZoneId);
				this._viewZoneId = undefined;
			}
		});
	}

	public updateLines(lineNumber: number, additionalLines: string[], minReservedLineCount: number): void {
		const textModel = this.editor.getModel();
		if (!textModel) {
			return;
		}

		const { tabSize } = textModel.getOptions();

		this.editor.changeViewZones((changeAccessor) => {
			if (this._viewZoneId) {
				changeAccessor.removeZone(this._viewZoneId);
				this._viewZoneId = undefined;
			}

			const heightInLines = Math.max(additionalLines.length, minReservedLineCount);
			if (heightInLines > 0) {
				const domNode = document.createElement('div');
				this.renderLines(domNode, tabSize, additionalLines, this.editor.getOptions());

				this._viewZoneId = changeAccessor.addZone({
					afterLineNumber: lineNumber,
					heightInLines: heightInLines,
					domNode,
				});
			}
		});
	}

	private renderLines(domNode: HTMLElement, tabSize: number, lines: string[], opts: IComputedEditorOptions): void {
		const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations);
		const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter);
		// To avoid visual confusion, we don't want to render visible whitespace
		const renderWhitespace = 'none';
		const renderControlCharacters = opts.get(EditorOption.renderControlCharacters);
		const fontLigatures = opts.get(EditorOption.fontLigatures);
		const fontInfo = opts.get(EditorOption.fontInfo);
		const lineHeight = opts.get(EditorOption.lineHeight);

		const sb = createStringBuilder(10000);
		sb.appendASCIIString('<div class="suggest-preview-text">');

		for (let i = 0, len = lines.length; i < len; i++) {
			const line = lines[i];
			sb.appendASCIIString('<div class="view-line');
			sb.appendASCIIString('" style="top:');
			sb.appendASCIIString(String(i * lineHeight));
			sb.appendASCIIString('px;width:1000000px;">');

			const isBasicASCII = strings.isBasicASCII(line);
			const containsRTL = strings.containsRTL(line);
			const lineTokens = LineTokens.createEmpty(line);

			renderViewLine(new RenderLineInput(
				(fontInfo.isMonospace && !disableMonospaceOptimizations),
				fontInfo.canUseHalfwidthRightwardsArrow,
				line,
				false,
				isBasicASCII,
				containsRTL,
				0,
				lineTokens,
				[],
				tabSize,
				0,
				fontInfo.spaceWidth,
				fontInfo.middotWidth,
				fontInfo.wsmiddotWidth,
				stopRenderingLineAfter,
				renderWhitespace,
				renderControlCharacters,
				fontLigatures !== EditorFontLigatures.OFF,
				null
			), sb);

			sb.appendASCIIString('</div>');
		}
		sb.appendASCIIString('</div>');

		Configuration.applyFontInfoSlow(domNode, fontInfo);
		const html = sb.build();
		const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html;
		domNode.innerHTML = trustedhtml as string;
	}
}

let keyCounter = 0;

function registerDecorationType(service: ICodeEditorService, description: string, keyPrefix: string, options: IDecorationRenderOptions) {
	const key = keyPrefix + (keyCounter++);
	service.registerDecorationType(description, key, options);
	return {
		dispose() {
			service.removeDecorationType(key);
		},
		resolve() {
			return service.resolveDecorationOptions(key, true);
		}
	};
}

class ViewMoreLinesContentWidget extends Disposable implements IContentWidget {
	readonly allowEditorOverflow = false;
	readonly suppressMouseDown = false;

	constructor(
		private editor: ICodeEditor,
		private position: Position,
		private domNode: HTMLElement,
		disposableStore: DisposableStore
	) {
		super();
		this._register(disposableStore);
		this._register(toDisposable(() => {
			this.editor.removeContentWidget(this);
		}));
		this.editor.addContentWidget(this);
	}

	getId(): string {
		return 'editor.widget.viewMoreLinesWidget';
	}

	getDomNode(): HTMLElement {
		return this.domNode;
	}

	getPosition(): IContentWidgetPosition | null {
		return {
			position: this.position,
			preference: [ContentWidgetPositionPreference.EXACT]
		};
	}
}

registerThemingParticipant((theme, collector) => {
	const foreground = theme.getColor(ghostTextForeground);

	if (foreground) {
		const opacity = String(foreground.rgba.a);
		const color = Color.Format.CSS.format(opaque(foreground))!;

		// We need to override the only used token type .mtk1
		collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { opacity: ${opacity}; color: ${color}; }`);
	}

	const border = theme.getColor(ghostTextBorder);
	if (border) {
		collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { border: 2px dashed ${border}; }`);
	}
});
